from collections import defaultdict
from typing import Any

from promise import Promise

from ....checkout.fetch import get_available_built_in_shipping_methods_for_checkout_info
from ....core.utils.events import get_is_deferred_payload
from ....webhook.event_types import WebhookEventSyncType
from ...app.dataloaders.apps import AppsByEventTypeLoader
from ...checkout.dataloaders.checkout_infos import (
    CheckoutInfoByCheckoutTokenLoader,
    CheckoutLinesInfoByCheckoutTokenLoader,
)
from ...core.dataloaders import DataLoader
from ..subscription_payload import (
    generate_payload_promise_from_subscription,
)
from ..utils import get_subscription_query_hash
from .models import WebhooksByEventTypeLoader
from .request_context import (
    PayloadsRequestContextByEventTypeLoader,
)


class PregeneratedCheckoutFilterShippingMethodPayloadsByCheckoutTokenLoader(DataLoader):
    context_key = (
        "pregenerated_checkout_filter_shipping_method_payloads_by_checkout_token"
    )

    def batch_load(self, keys):
        """Fetch pregenerated tax payloads for checkout shipping method filtering.

        This loader is used to fetch pregenerated payloads for checkouts shipping method filtering.

        return: A dict of shipping method filtering payloads for checkouts.

        Example:
        {
            "checkout_token": {
                "app_id": {
                    "query_hash": {
                        <payload>
                    }
                }
            }
        }

        """

        if requested_by_event_type := getattr(self.context, "event_type", None):
            # Skip sync payload generation when request is generated by
            # subscription for non-deferred ASYNC webhook. Sync webhooks in that case
            # are called before async webhook events.
            if not get_is_deferred_payload(requested_by_event_type):
                return [None for _ in keys]

        results: dict[str, dict[int, dict[str, dict[str, Any]]]] = defaultdict(
            lambda: defaultdict(dict)
        )

        event_type = WebhookEventSyncType.CHECKOUT_FILTER_SHIPPING_METHODS

        def with_webhooks(webhooks):
            if not webhooks:
                return [None for _ in keys]

            def generate_payloads(data):
                checkout_infos, lines_infos, apps, request_context = data
                apps_map = {app.id: app for app in apps}
                promises = []
                for checkout_info, lines_info in zip(
                    checkout_infos, lines_infos, strict=False
                ):
                    checkout = checkout_info.checkout
                    checkout_info.lines_info = lines_info
                    built_in_shipping_methods = (
                        get_available_built_in_shipping_methods_for_checkout_info(
                            checkout_info
                        )
                    )

                    for webhook in webhooks:
                        if not webhook.subscription_query:
                            continue
                        query_hash = get_subscription_query_hash(
                            webhook.subscription_query
                        )
                        app_id = webhook.app_id
                        app = apps_map[app_id]
                        checkout_token = str(checkout.token)
                        promise_payload = generate_payload_promise_from_subscription(
                            event_type=event_type,
                            subscribable_object=(checkout, built_in_shipping_methods),
                            subscription_query=webhook.subscription_query,
                            request=request_context,
                            app=app,
                        )

                        def store_payload(
                            payload,
                            checkout_token=checkout_token,
                            app_id=app_id,
                            query_hash=query_hash,
                        ):
                            if payload:
                                results[checkout_token][app_id][query_hash] = payload

                        promises.append(promise_payload.then(store_payload))

                def return_payloads(_payloads):
                    return [
                        results[str(checkout_info.checkout.token)]
                        for checkout_info in checkout_infos
                    ]

                return Promise.all(promises).then(return_payloads)

            apps = AppsByEventTypeLoader(self.context).load(event_type)
            request_context = PayloadsRequestContextByEventTypeLoader(
                self.context
            ).load(event_type)
            lines_infos = CheckoutLinesInfoByCheckoutTokenLoader(
                self.context
            ).load_many(keys)
            checkout_infos = CheckoutInfoByCheckoutTokenLoader(self.context).load_many(
                keys
            )
            return Promise.all(
                [checkout_infos, lines_infos, apps, request_context]
            ).then(generate_payloads)

        return (
            WebhooksByEventTypeLoader(self.context).load(event_type).then(with_webhooks)
        )
